/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file audio.cpp
 * @author TooOld2Rock'nRoll
 * @see audio.hpp
 */
/*---- Includes ----*/
#include "audio/audio.hpp"

#include <glm/fwd.hpp>
#include <glm/geometric.hpp>

#include "toolbox/debug.h"


/*---- Defines ----*/
#ifndef AUDIO_SUPPORTED_FORMATS
///A fallback configuration in case ArcadeFighter's config.h file is not present in the compilation tree.
#define AUDIO_SUPPORTED_FORMATS     MIX_INIT_MP3
#endif


/*---- Global Variables ----*/
#ifdef ENABLE_DEBUG
//Private array with names for the soud states to be printed on messages.
//IMPORTANT! If the enumeration changes, this MUST change too.
static const char *_sound_state_names[sound_state_e::_sound_state_max + 1] =
{"Loaded", "Fading_in", "Playing", "Paused", "Fading_out", "Stopped", "STATE_MAX"};
#endif //ENABLE_DEBUG


/*---- Local Functions ----*/
/**
 * @brief Converts world distances to SDL volume range, works just like Arduino's map function.
 * @see https://docs.arduino.cc/language-reference/en/functions/math/map/
 *
 * @param x - the number to map.
 * @param in_min - the lower bound of the value’s target range.
 * @param in_max - the upper bound of the value’s target range.
 *
 * @return The mapped value.
 */
float fromDistance2Volume (float x, float in_min, float in_max)
{
      return (x - in_min) * 255 / (in_max - in_min);
}//end fromDistance2Volume ()


/*---- Methods Implementation ----*/
/** @brief Regulates state changes for Music Containers.
 *
 * Here we only offer a simple state machine to keep the playback life cycle consistent and a good way to extend it would
 *   be by calling the superclass method first and if it returns true, proceed with your own customization. Forcing the
 *   state change to return true may place the entire playback life cycle in a inconsistent sate that would be difficult
 *   to manage outside the base class otherwise.
 *
 * @param new_state - the state the container will go to.
 *
 * @return <b>true</b> if state change was accepted and the procedure may proceed or <b>false</b> if anything is wrong.
 *
 * @todo When virtualization is a thing, we can´t let music be virtualized!
 */
bool Audio::MusicFile::setState (sound_state_e new_state)
{
    bool confirm = false;

    _debug (TRACE, "::%s (st=%s)", __FUNCTION__, _sound_state_names[new_state]);

    if (this->e_state == new_state)
        return true; //just ignore it....

    switch (new_state)
    {
        case sound_state_e::SOUND_LOADED:
            if (this->e_state == sound_state_e::_sound_state_max)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_LOADED:

        case sound_state_e::SOUND_FADING_IN:
            if (this->e_state == sound_state_e::SOUND_LOADED ||
                this->e_state == sound_state_e::SOUND_FADING_OUT ||
                this->e_state == sound_state_e::SOUND_STOPPED)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_FADING_IN:

        case sound_state_e::SOUND_PLAYING:
            if (this->e_state == sound_state_e::SOUND_LOADED ||
                this->e_state == sound_state_e::SOUND_FADING_IN ||
                this->e_state == sound_state_e::SOUND_STOPPED)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_PLAYING:

        case sound_state_e::SOUND_PAUSED:
            if (this->e_state == sound_state_e::SOUND_FADING_IN ||
                this->e_state == sound_state_e::SOUND_PLAYING ||
                this->e_state == sound_state_e::SOUND_FADING_OUT)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_PAUSED:

        case sound_state_e::SOUND_FADING_OUT:
            if (this->e_state == sound_state_e::SOUND_FADING_IN ||
                this->e_state == sound_state_e::SOUND_PLAYING)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_FADING_OUT:

        case sound_state_e::SOUND_STOPPED:
            //any state can go to stopped!
            this->d_playback_time = 0.f;

            confirm = true;
            break;
        //end case sound_state_e::SOUND_PAUSED:

        case sound_state_e::_sound_state_max:
        default:
            _debug (ERROR, "We don´t recognize this state %u for Music container state change!", new_state);
            return false;
    }//end switch (new_state)

    if (confirm)
        this->e_state = (sound_state_e)new_state;
#ifdef ENABLE_DEBUG
    else
        _debug (WARNING, "Possible error, invalid Music container state change, from %s to %s!",
                                                    _sound_state_names[this->e_state], _sound_state_names[new_state]);
#endif //ENABLE_DEBUG

    return confirm;
}//End setState ())


/**
 * @brief Saves the play head position (time playing) and handles fading effects.
 * @remarks If overriding this method, please remember to call the superclass first as it process important information
 *     about the contained playback life cycle.
 *
 * @param delta_t - time elapsed since last loop iteration.
 */
void Audio::MusicFile::update (double delta_t)
{
    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    if (this->e_state != sound_state_e::SOUND_FADING_IN &&
            this->e_state != sound_state_e::SOUND_PLAYING &&
            this->e_state != sound_state_e::SOUND_FADING_OUT)
        return;

    this->d_playback_time = Mix_GetMusicPosition (this->getSource()->getData ());

    if (this->e_state == sound_state_e::SOUND_FADING_IN &&
            this->d_playback_time >= this->d_fadein)
    {
        if (!this->setState (sound_state_e::SOUND_PLAYING))
        {
            this->setState (sound_state_e::SOUND_STOPPED);
            Mix_HaltMusic ();
        }//end if (!this->setState (sound_state_e::SOUND_PLAYING))
    }//end if (this->e_state == sound_state_e::SOUND_FADING_IN && ...

    else if (this->d_fadeout &&
            this->e_state != sound_state_e::SOUND_FADING_OUT &&
            this->d_playback_time + this->d_fadeout >= this->getSource()->getLength())
    {
        if (this->setState (sound_state_e::SOUND_FADING_OUT))
            Mix_FadeOutMusic (this->d_fadeout * 1000);
        else
        {
            this->setState (sound_state_e::SOUND_STOPPED);
            Mix_HaltMusic ();
        }//end else
    }//end else if ()
}//End update ()

/********************************************************************/

/** @brief Regulates state changes for Music Streaming Containers.
 *
 * Here we only offer a simple state machine to keep the playback life cycle consistent and a good way to extend it would
 *   be by calling the superclass method first and if it returns true, proceed with your own customization. Forcing the
 *   state change to return true may place the entire playback life cycle in a inconsistent sate that would be difficult
 *   to manage outside the base class otherwise.
 *
 * @param new_state - the state the container will go to.
 *
 * @return <b>true</b> if state change was accepted and the procedure may proceed or <b>false</b> if anything is wrong.
 *
 * @todo When virtualization is a thing, we can´t let music be virtualized!
 */
bool Audio::MusicStreaming::setState (sound_state_e new_state)
{
    bool confirm = false;

    _debug (TRACE, "::%s (st=%s)", __FUNCTION__, _sound_state_names[new_state]);

    if (this->e_state == new_state)
        return true; //just ignore it....

    switch (new_state)
    {
        case sound_state_e::SOUND_LOADED:
            if (this->e_state == sound_state_e::_sound_state_max)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_LOADED:

        //Music streams can´t be faded...
        // case sound_state_e::SOUND_FADING_IN:

        case sound_state_e::SOUND_PLAYING:
            if (this->e_state == sound_state_e::SOUND_LOADED ||
                this->e_state == sound_state_e::SOUND_FADING_IN ||
                this->e_state == sound_state_e::SOUND_STOPPED)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_PLAYING:

        case sound_state_e::SOUND_PAUSED:
            if (this->e_state == sound_state_e::SOUND_FADING_IN ||
                this->e_state == sound_state_e::SOUND_PLAYING ||
                this->e_state == sound_state_e::SOUND_FADING_OUT)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_PAUSED:

        //Music streams can´t be faded...
        // case sound_state_e::SOUND_FADING_OUT:

        case sound_state_e::SOUND_STOPPED:
            //any state can go to stopped!
            this->d_playback_time = 0.f;

            confirm = true;
            break;
        //end case sound_state_e::SOUND_PAUSED:

        case sound_state_e::_sound_state_max:
        default:
            _debug (ERROR, "We don´t recognize this state %u for Music Streaming container state change!", new_state);
            return false;
    }//end switch (new_state)

    if (confirm)
        this->e_state = (sound_state_e)new_state;
#ifdef ENABLE_DEBUG
    else
        _debug (WARNING, "Possible error, invalid Music Streaming container state change, from %s to %s!",
                                                    _sound_state_names[this->e_state], _sound_state_names[new_state]);
#endif //ENABLE_DEBUG

    return confirm;
}//End MusicStreaming::setState ()


/** @brief Just adds delta_t to the playback time.
 * @param delta_t - time elapsed since last loop iteration.
 */
void Audio::MusicStreaming::update (double delta_t)
{
    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    if (this->e_state == sound_state_e::SOUND_PLAYING)
        this->d_playback_time = this->d_playback_time + delta_t;//atomics have no operator+= for double
}//End MusicStreaming::update ()

/********************************************************************/

/** @brief Regulates state changes for Effecs Containers.
 *
 * Here we only offer a simple state machine to keep the playback life cycle consistent and a good way to extend it would
 *   be by calling the superclass method first and if it returns true, proceed with your own customization. Forcing the
 *   state change to return true may place the entire playback life cycle in a inconsistent sate that would be difficult
 *   to manage outside the base class otherwise.
 *
 * @param new_state - the state the container will go to.
 *
 * @return <b>true</b> if state change was accepted and the procedure may proceed or <b>false</b> if anything is wrong.
 */
bool Audio::Effect::setState (sound_state_e new_state)
{
    bool confirm = false;

    _debug (TRACE, "::%s (st=%s)", __FUNCTION__, _sound_state_names[new_state]);

    if (this->e_state == new_state)
        return true; //just ignore it....

    switch (new_state)
    {
        case sound_state_e::SOUND_LOADED:
            if (this->e_state == sound_state_e::_sound_state_max)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_LOADED:

        case sound_state_e::SOUND_FADING_IN:
            if (this->e_state == sound_state_e::SOUND_LOADED ||
                this->e_state == sound_state_e::SOUND_FADING_OUT ||
                this->e_state == sound_state_e::SOUND_STOPPED)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_FADING_IN:

        case sound_state_e::SOUND_PLAYING:
            if (this->e_state == sound_state_e::SOUND_LOADED ||
                this->e_state == sound_state_e::SOUND_FADING_IN ||
                this->e_state == sound_state_e::SOUND_STOPPED)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_PLAYING:

        case sound_state_e::SOUND_PAUSED:
            if (this->e_state == sound_state_e::SOUND_FADING_IN ||
                this->e_state == sound_state_e::SOUND_PLAYING ||
                this->e_state == sound_state_e::SOUND_FADING_OUT)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_PAUSED:

        case sound_state_e::SOUND_FADING_OUT:
            if (this->e_state == sound_state_e::SOUND_FADING_IN ||
                this->e_state == sound_state_e::SOUND_PLAYING)
                confirm = true;
            break;
        //end case sound_state_e::SOUND_FADING_OUT:

        case sound_state_e::SOUND_STOPPED:
            //any state can go to stopped!
            this->_using_track = INVALID_TRACK_ID;
            this->u_repeat_count = 0;
            this->d_playback_time = 0.f;

            confirm = true;
            break;
        //end case sound_state_e::SOUND_PAUSED:

        case sound_state_e::_sound_state_max:
        default:
            _debug (ERROR, "We don´t recognize this state %u for Effect container state change!", new_state);
            return false;
    }//end switch (new_state)

    if (confirm)
        this->e_state = (sound_state_e)new_state;
#ifdef ENABLE_DEBUG
    else
        _debug (WARNING, "Possible error, invalid Effect container state change, from %s to %s!",
                                                    _sound_state_names[this->e_state], _sound_state_names[new_state]);
#endif //ENABLE_DEBUG

    return confirm;
}//End setState ()

/**
 * @brief Adds delta_t to the playback time and handles fading effects.
 * @param delta_t - time elapsed since last loop iteration.
 */
void Audio::Effect::update (double delta_t)
{
    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    if (this->e_state != sound_state_e::SOUND_FADING_IN &&
            this->e_state != sound_state_e::SOUND_PLAYING &&
            this->e_state != sound_state_e::SOUND_FADING_OUT)
        return;

    this->d_playback_time = this->d_playback_time + delta_t;//atomics have no operator+= for double

    if (this->e_state == sound_state_e::SOUND_FADING_IN &&
            this->d_playback_time >= this->d_fadein)
    {
        if (!this->setState (sound_state_e::SOUND_PLAYING))
        {
            this->setState (sound_state_e::SOUND_STOPPED);
            if (this->_using_track >= 0)//be sure is not virtualized...
                Mix_HaltChannel (this->_using_track);
        }//end if (!this->setState (sound_state_e::SOUND_PLAYING))
    }//end if (this->e_state == sound_state_e::SOUND_FADING_IN && ...

    else if (this->d_fadeout &&
            this->e_state != sound_state_e::SOUND_FADING_OUT &&
            this->d_playback_time + this->d_fadeout >= this->getSource()->getLength())
    {
        if (this->setState (sound_state_e::SOUND_FADING_OUT))
            Mix_FadeOutChannel (this->_using_track, this->d_fadeout * 1000);
        else
        {
            this->setState (sound_state_e::SOUND_STOPPED);
            if (this->_using_track >= 0)//be sure is not virtualized...
                Mix_HaltChannel (this->_using_track);
        }//end else
    }//end else if ()
}//End update ()


/********************************************************************/


/**
 * @brief Default constructor.
 * @throws std::runtime_error - in case the audio interface can not be initialized.
 */
Audio::Audio ()
{
    _debug (INFO, "::%s () <--The Audio singleton has been created!", __FUNCTION__);

    if (Mix_Init (AUDIO_SUPPORTED_FORMATS) < 0)
        throw std::runtime_error (fmt::format ("{0}:{1} - Failed to init SDL_Mixer, error = {2}!", __LINE__, __FILE__, Mix_GetError()));

    _debug (INFO, "SDL Mixer (the native audio API) has been initialized successfully!");

    this->b_is_open = false;

    //track count should be the same as MIX_CHANNELS, the default for SDL_Mixer
    this->setTrackCount (MIX_CHANNELS);

    Mix_HookMusicFinished (Audio::_music_finished_cb);
    Mix_ChannelFinished (Audio::_track_finished_cb);
}//End Constructor


/**
 * @brief Access the singleton instance.
 *
 * @return Reference to the Audio singleton.
 */
Audio& Audio::instance ()
{
    static Audio _instance;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    return _instance;
}//End instance ()


/**
 * @brief Destructor.
 */
Audio::~Audio ()
{
    _debug (INFO, "::%s () <--The Audio singleton will be destroyed!", __FUNCTION__);

    if (this->b_is_open)
        this->close ();

    if (this->a_tracks)
        delete[] this->a_tracks;

    Mix_Quit ();
}//End Destructor


/**
 * @brief Opens a audio device to play audio, must be called before any audio can actually be played.
 * @remark Requires to be closed when no longer necessary, definitely before the program is finished.
 *
 * @param frequency - the frequency (in Hz) to play the audio, defaults to MIX_DEFAULT_FREQUENCY.
 * @param format - the audio device format, defaults to MIX_DEFAULT_FORMAT.
 * @param channels - how many channels to use: 1(mono), 2(stereo), 4(quad), 6(5.1), etc, defaults to MIX_DEFAULT_CHANNELS.
 * @param chunksize - number of sample frames to use (it's like buffer size), defaults to 2048.
 * @param device - <b>nullptr</b> to use a best guess (aka the system default option, works most of the time).
 *
 * @return <b>false</b> if no errors, <b>true</b> if there was a error and the audio device could not be opened.
 * @see https://wiki.libsdl.org/SDL2_mixer/Mix_OpenAudioDevice
 */
bool Audio::open (unsigned frequency, unsigned short format, unsigned channels, unsigned chunksize, const char *device)
{
    _debug (TRACE, "::%s (Hz=%u, form=%u, ch=%u, buff=%u, device=%s)", __FUNCTION__, frequency, format, channels, chunksize, device);

    if (this->b_is_open)
    {
        _debug (ERROR, "Audio Device already open, can't open two devices at the same time!");
        return true;
    }//end if (this->b_is_open)
    else if (Mix_OpenAudioDevice(frequency, format, channels, chunksize, device, SDL_AUDIO_ALLOW_ANY_CHANGE) < 0)
    {
        _debug (ERROR, "The %s audio device can't be opened, error:\r\n%s", device, Mix_GetError());
        return true;
    }//end else if (Mix_OpenAudioDevice(frequency, format, channels, chunksize, device, SDL_AUDIO_ALLOW_ANY_CHANGE) < 0)

    this->b_is_open = true;

    //since we permit the Mixer to adapt the user parameters to the next best available configuration,
    //we need to confirm the actual values supported by the device.
    if (device)
        this->st_audio_device_info.s_device_name = device;
    Mix_QuerySpec ((int *)&this->st_audio_device_info.u_frequency,
                   (Uint16 *)&this->st_audio_device_info.t_format,
                   (int *)&this->st_audio_device_info.u_channels);
    this->st_audio_device_info.u_chunksize = chunksize;
    _debug (INFO, "%s audio device has been successfully opened with Hz=%u, format=%u, channels=%u", device, this->st_audio_device_info.u_frequency, format, this->st_audio_device_info.u_channels);

    _debug (INFO, "Starting the Audio working thread...");
    if (this->p_service_thread)
        throw std::runtime_error (fmt::format ("{0}:{1} - Working thread is not null, this should not be possible!", __LINE__, __FILE__));

    this->p_service_thread = new std::thread (&Audio::_serviceThread_cb, this);

    return false;
}//End open ()


/**
 * @brief Static method that retrieves a list of device names capable of plating audio in this system.
 *
 * @param[out] device_list - reference to a vector to be populated by strings with the name of supported Audio Devices.
 */
void Audio::getAudioDevicesList (std::vector<std::string> &device_list)
{
    int devices_count = SDL_GetNumAudioDevices (0);

    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (devices_count <= 0)
    {
        _debug (ERROR, "Something went wrong getting the list of Audio Device Names, error:\r\n%s", SDL_GetError());
        return;
    }//end if (devices_count <= 0)

    device_list.reserve (devices_count);
    _debug (INFO, "List of Audio Devices:");
    for ( ; devices_count--; )
    {
        device_list.push_back (SDL_GetAudioDeviceName (devices_count, 0));
        _debug (INFO, "\t%s", device_list.back().c_str ());
    }//end for ( ; devices_count--; )
}//End getAudioDevicesList ()


/**
 * @brief Closes an open Audio Device.
 */
void Audio::close ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (!this->b_is_open)
        return; //just ignores.....

     Mix_CloseAudio ();
     this->b_is_open = false;
     _debug (INFO, "Audio Device was closed!");

     if (this->p_service_thread)
     {
         _debug (INFO, "Killing the Audio working thread, waiting to finish jobs...");
         if (this->p_service_thread->joinable ())
             this->p_service_thread->join ();
         delete this->p_service_thread; //this should kill the thread if active....
     }//end if (this->p_service_thread)
}//End close ()


/**
 * @brief Mutes the entire Audio API.
 *
 * @param m - <b>true</b> to mute, <b>false</b> to unmute.
 */
void Audio::mute (bool m)
{
    _debug (TRACE, "::%s (mute=%s)", __FUNCTION__, (m ? "true" : "false"));

    this->b_mute = m;
    if (m)
        Mix_MasterVolume (0);
    else
        Mix_MasterVolume (MIX_MAX_VOLUME * this->f_master_volume);
}//End mute ()


/**
 * @brief Sets a base volume to which all other sound volumes are mixed against.
 *
 * @param volume - from 0 to 100, works like a % to the other sounds.
 */
void Audio::setMasterVol (unsigned short volume)
{
    _debug (TRACE, "::%s (vol=%u)", __FUNCTION__, volume);

    this->f_master_volume = (volume <= 100 ? volume/100.f : 1.f);

    if (!this->b_mute)
        Mix_MasterVolume (MIX_MAX_VOLUME * this->f_master_volume);
}//End setMasterVolume ()


/**
 * @brief Pauses the entire Audio library.
 */
void Audio::pause ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    this->pauseMusic ();
    this->pauseAllEffects ();
}//End pause ()


/**
 * @brief Resumes the entire Audio library.
 */
void Audio::resume ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    this->resumeMusic ();
    this->resumeAllEffects ();
}//End resume ()


/**
 * @brief Halts the Audio library (Stops every sound being played!).
 */
void Audio::halt ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    this->stopMusic ();
    this->stopAllEffects ();
}//End halt ()


/**
 * @brief The method that performs background tasks for the Audio service thread.
 * It will run unbound (as fast as possible) considering audio is a real time priority service!
 */
void Audio::_serviceThread_cb ()
{
    //This will be much more useful once we "upgrade" to SDL Sound and manage the buffers ourselves....
    int i = 0;
    double delta_t = 0.f;

    float distance = 0.f;
    //to centers listener coordinate on [0,0].
    glm::vec3 ref;
    //we only need a 2D reference to position the effect in the world.
    glm::vec2 norm_ref;

    while (this->b_is_open)
    {
        delta_t = getDeltaTime (&this->_thread_delta_t, true);

        this->update (delta_t);

        if (this->p_music_playing)
            this->p_music_playing->update (delta_t);

        for (i = this->u_track_count; i--; )
        {
           if (this->a_tracks[i])
              this->a_tracks[i]->update (delta_t);
        }//end for (i = this->u_track_count; i--; )

        //recalculate the relative position of all sounds
        //this is done here instead of the update method since the "listener position" will generally be an important
        //   game immersion tool and should be updated in real time.
        if (this->v3_microphone_position.isDirty ())
        {
            for (i = this->u_track_count; i--; )
            {
                if (!this->a_tracks[i])
                    continue;
                else if (this->a_tracks[i]->isImportant ())
                {
                    ref = this->a_tracks[i]->v3_position.get() - this->v3_microphone_position.get();
                    distance = glm::length (ref);
                    if (distance > this->f_min_distance)
                    {
                        norm_ref = glm::normalize (ref);
                        Mix_SetPosition (this->a_tracks[i]->_using_track, std::atan2(norm_ref.x, norm_ref.y) * 180 / 3.14,
                                         fromDistance2Volume (distance, this->f_min_distance, this->f_max_distance));
                    }//end if (distance > this->f_min_distance)
                }//end else if (this->a_tracks[i]->isImportant ())

                this->a_tracks[i]->v3_position.resetFlag ();
            }//end for (i = this->u_track_count; i--; )

            this->v3_microphone_position.resetFlag ();
        }//end else if (this->a_tracks[i]->v3_position.isDirty ())

        //need to free the thread to give time for other processes
        sleep (0.000001);
        // printf ("Audio Service Thread Hz: target=∞ -- real=%.4f\r", 1.0/getDeltaTime (&this->_thread_delta_t, false)); //<-- debug
    }//end while (this->b_is_open)

    _debug (TRACE, "Audio working thread finished...");
}//End _serviceThread_cb ()


/**
 * @brief Updates the Audio API state.
 * @warning We <b>only</b> invalidate pointers to the audio tracks here!
 *
 * @param delta_t - time elapsed since last loop iteration.
 */
void Audio::update (double delta_t)
{
    int i = 0;

    static float distance = 0.f;
    //to centers listener coordinate on [0,0].
    static glm::vec3 ref;
    //we only need a 2D reference to position the effect in the world.
    static glm::vec2 norm_ref;

    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    if (this->p_music_playing && this->p_music_playing->getState() == sound_state_e::SOUND_STOPPED)
        this->p_music_playing = nullptr;
    else if (this->p_music_streaming && this->p_music_streaming->getState() == sound_state_e::SOUND_STOPPED)
        this->p_music_streaming = nullptr;

    for (i = this->u_track_count; i--; )
    {
        if (!this->a_tracks[i])
            continue;
        //check for Effects that are no longer valid
        else if (this->a_tracks[i]->e_state == sound_state_e::SOUND_STOPPED)
            this->a_tracks[i] = nullptr;
        //check for important effects that changed position
        else if (this->a_tracks[i]->v3_position.isDirty ())
        {
            if (this->a_tracks[i]->isImportant ())
            {
                ref = this->a_tracks[i]->v3_position.get() - this->v3_microphone_position.get();
                distance = glm::length (ref);
                if (distance > this->f_min_distance)
                {
                    norm_ref = glm::normalize (ref);
                    Mix_SetPosition (this->a_tracks[i]->_using_track, std::atan2(norm_ref.x, norm_ref.y) * 180 / 3.14,
                                     fromDistance2Volume (distance, this->f_min_distance, this->f_max_distance));
                }//end if (distance > this->f_min_distance)
            }//end else if (this->a_tracks[i]->isImportant ())

            this->a_tracks[i]->v3_position.resetFlag ();
        }//end else if (this->a_tracks[i]->v3_position.isDirty ())
    }//end for (i = this->u_track_count; i--; )
}//End update ()


/**
 * @brief Internal method that gets notified when a Mix_Music finished playing.
 * @remarks Does not trigger on repeat.
 */
void Audio::_music_finished_cb ()
{
    Audio &obj_this = Audio::instance ();

    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (!obj_this.p_music_playing)
        return; //just ignore, it may have been removed somewhere else.
    else if (obj_this.p_music_playing->getState() == sound_state_e::SOUND_STOPPED)
        return; //music has been stopped somewhere else...

    if (obj_this.p_music_playing->isOnLoop ())
    {
        obj_this.p_music_playing->d_playback_time = 0;

        if (obj_this.p_music_playing->d_fadein &&
                !obj_this.p_music_playing->setState (sound_state_e::SOUND_FADING_IN))
        {
            _debug (ERROR, "This is strange, but trying to loop the Music to Fade In has been denied! Halting playback...");
            obj_this.p_music_playing->setState (sound_state_e::SOUND_STOPPED);
        }
        else //proceed if no errors...
        {
            if (Mix_FadeInMusic (obj_this.p_music_playing->getSource()->getData (), 0, obj_this.p_music_playing->d_fadein * 1000) < 0)
            {
                _debug (ERROR, "Error trying to loop the music playback, this may or may not help:\r\n%s", Mix_GetError());
                obj_this.p_music_playing->setState (sound_state_e::SOUND_STOPPED);
            }
        }//end else
    }//end if (obj_this.p_music_playing->isLoop ())
    else
        obj_this.p_music_playing->setState (sound_state_e::SOUND_STOPPED);
}//End _music_finished_cb ()


/**
 * @brief To control attenuation and panning for spatial audio.
 *
 * Any distance smaller than min will be centered played with max volume.<br>
 * Any distance greater than max will be panned and played with no volume (muted);
 *
 * @param min - the minimum world distance to change the sounds position.
 * @param max - the maximum world distance to change the sounds position.
 */
void Audio::setWorldDistances (float min, float max)
{
    _debug (TRACE, "::%s (min=%4.f, max=%4.f)", __FUNCTION__, min, max);

    this->f_min_distance = min;
    this->f_max_distance = max;
}//End setWorldDistances ()


/**
 * @brief Sets a base volume to which all other music volumes are mixed against.
 *
 * @param volume - from 0 to 100, works like a % to the other sounds.
 */
void Audio::setMusicsVol (unsigned short volume)
{
    _debug (TRACE, "::%s (vol=%u)", __FUNCTION__, volume);

    this->us_musics_volume = (volume <= 100 ? volume/100.f: 1.f);

    if (this->isPlayingMusic ())
    {
        if (this->p_music_playing)
            Mix_VolumeMusic (MIX_MAX_VOLUME * ((this->p_music_playing->getSource()->getVol() * this->us_musics_volume)/100.f));
        else if (this->p_music_streaming)
            Mix_VolumeMusic (MIX_MAX_VOLUME * ((this->p_music_streaming->getSource()->getVol() * this->us_musics_volume)/100.f));
    }//end if (this->isPlayingMusic ())
}//End setMusicsVol ()


/**
 * @brief Start playback of a Music (large audio).
 *
 * If any music was already playing, it will be stopped and replaced by the new one.<br>
 * A null music source will just stop playback if any.
 *
 * @param mu - pointer to a music track to play.
 *
 * @throws std::invalid_argument - in case the container has no source.
 */
void Audio::play (MusicFile *mu)
{
    _debug (TRACE, "::%s (music file)", __FUNCTION__);

    if (!this->b_is_open)
    {
        _debug (ERROR, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)

    this->stopMusic ();

    this->p_music_playing = mu;
    if (!mu)
        return; //just ignore.
    else if (!mu->getSource ())
        throw std::invalid_argument (fmt::format ("{0}:{1} - Trying to play a Music container with no source!", __LINE__, __FILE__));

    if (this->p_music_playing->setState ((mu->d_fadein > 0 ? sound_state_e::SOUND_FADING_IN : sound_state_e::SOUND_PLAYING)))
    {
        Mix_VolumeMusic (MIX_MAX_VOLUME * ((mu->getSource()->getVol()  * this->us_musics_volume)/100.f));

        if (Mix_FadeInMusic (mu->getSource()->getData (), 0, mu->d_fadein * 1000) < 0)
            _debug (ERROR, "Something went wrong trying to start to play a music, this may or may not help:\r\n%s", Mix_GetError());
    }//end if (data->setState (sound_state_e::SOUND_PLAYING))
}//End play (music file)


/**
 * @brief Start playback of a Music Streaming.
 *
 * If any music was already playing, it will be stopped and replaced by the new one.<br>
 * A null music source will just stop playback if any.
 *
 * @param mu - pointer to a music stream to play.
 *
 * @throws std::invalid_argument - in case the container has no source.
 */
void Audio::play (MusicStreaming *mu)
{
    _debug (TRACE, "::%s (music streaming)", __FUNCTION__);

    if (!this->b_is_open)
    {
        _debug (ERROR, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)

    this->stopMusic ();

    this->p_music_streaming = mu;
    if (!mu)
        return; //just ignore.
    else if (!mu->getSource ())
        throw std::invalid_argument (fmt::format ("{0}:{1} - Trying to play a Music container with no source!", __LINE__, __FILE__));

    if (this->p_music_streaming->setState (sound_state_e::SOUND_PLAYING))
    {
        Mix_VolumeMusic (MIX_MAX_VOLUME * ((mu->getSource()->getVol()  * this->us_musics_volume)/100.f));
        Mix_HookMusic (p_music_streaming->getSource()->getData (), p_music_streaming->getSource ());
    }//end if (data->setState (sound_state_e::SOUND_PLAYING))
}//End play (music straming)


/**
 * @brief Pauses a Music playback.
 */
void Audio::pauseMusic ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (!this->b_is_open)
    {
        _debug (WARNING, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)

    if (this->p_music_playing && !this->p_music_playing->setState (sound_state_e::SOUND_PAUSED))
        return;
    else if (this->p_music_streaming && !this->p_music_streaming->setState (sound_state_e::SOUND_PAUSED))
        return;

    Mix_PauseMusic ();
}//End pauseMusic ()


/**
 * @brief Resume a Music playback.
 */
void Audio::resumeMusic ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (!this->b_is_open)
    {
        _debug (WARNING, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)

    if (this->p_music_playing && !this->p_music_playing->setState (sound_state_e::SOUND_PLAYING))
        return;
    else if (this->p_music_streaming && !this->p_music_streaming->setState (sound_state_e::SOUND_PLAYING))
        return;

    Mix_ResumeMusic ();
}//End resumeMusic ()


/**
 * @brief Completely stops a Music playback.
 *
 * @param fade_out - time in seconds to fade out Music (it will override the defined container fade out).
 */
void Audio::stopMusic (double fade_out)
{
    _debug (TRACE, "::%s (fade=%.4fs)", __FUNCTION__, fade_out);

    if (!this->b_is_open)
    {
        _debug (WARNING, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)

    if (this->p_music_playing &&
        this->p_music_playing->setState ((fade_out > 0 ? sound_state_e::SOUND_FADING_OUT : sound_state_e::SOUND_STOPPED)))
        Mix_FadeOutMusic (fade_out * 1000);
    else if (this->p_music_streaming && this->p_music_streaming->setState (sound_state_e::SOUND_STOPPED))
        Mix_HookMusic (nullptr, nullptr); ///<@todo Test if this will trigger the music stop callback!
}//End stopMusic ()


/**
 * @brief Change the number of pre allocated tracks to play Effects simultaneously.
 * @warning All currently playing Effects will be halted and dumped!
 *
 * @param num_tracks - the new number of tracks to use.
 *
 * @throws std::invalid_argument - in case num_tracks is zero.
 */
void Audio::setTrackCount (unsigned num_tracks)
{
    int i = 0;

    _debug (TRACE, "::%s (n_ch=%u)", __FUNCTION__, num_tracks);

    if (num_tracks == 0)
        throw std::invalid_argument (fmt::format ("{0}:{1} - Can't have zero tracks allocated!", __LINE__, __FILE__));
    else if (this->u_track_count == num_tracks)
        return; //just ignore....

    this->stopAllEffects ();
    if (this->a_tracks)
        delete []this->a_tracks;

    Mix_AllocateChannels (num_tracks);

    this->u_track_count = num_tracks;
    this->a_tracks = new Audio::Effect *[num_tracks];
    for (i = num_tracks; i--; )
        this->a_tracks[i] = nullptr;
}//End setTrackCount ()


/**
 * @brief Sets a base volume to which all other effect volumes are mixed against.
 *
 * @param volume - from 0 to 100, works like a % to the other sounds.
 */
void Audio::setSoundEffectsVol (unsigned short volume)
{
    _debug (TRACE, "::%s (vol=%u)", __FUNCTION__, volume);

    this->us_effects_volume = (volume <= 100 ? volume : 100);
    Mix_Volume (-1, MIX_MAX_VOLUME * (this->us_effects_volume / 100.f));
}//end setSoundEffectsVol ()


/**
 * @brief Internal method that gets notified when a Mix_Chunk finished playing.
 * @remarks Does not trigger on repeat.
 *
 * @param track - the "channel" number.
 */
void Audio::_track_finished_cb (AudioTrackID_t track)
{
    Audio &obj_this = Audio::instance ();

    _debug (VERBOSE, "::%s (track=%u)", __FUNCTION__, track);

    if (!obj_this.a_tracks[track])
        return; //just ignore, it may have been removed somewhere else.
    else if (obj_this.a_tracks[track]->getState() == sound_state_e::SOUND_STOPPED)
        return; //effect has been stopped somewhere else...

    if (obj_this.a_tracks[track]->u_repeat_count > 0 || obj_this.a_tracks[track]->isOnLoop ())
    {
        obj_this.a_tracks[track]->d_playback_time = 0;

        if (obj_this.a_tracks[track]->u_repeat_count > 0)
            --obj_this.a_tracks[track]->u_repeat_count;

        if (obj_this.a_tracks[track]->d_fadein &&
                !obj_this.a_tracks[track]->setState (sound_state_e::SOUND_FADING_IN))
        {
            _debug (ERROR, "This is strange, but trying to loop the Effect to Fade In has been denied! Halting playback...");
            obj_this.a_tracks[track]->setState (sound_state_e::SOUND_STOPPED);
        }
        else //proceed if no errors...
        {
            if (Mix_FadeInChannel (track, obj_this.a_tracks[track]->getSource()->getData (), 0, obj_this.a_tracks[track]->d_fadein * 1000) < 0)
            {
                _debug (ERROR, "Error trying to repeat effect playback, this may or may not help:\r\n%s", Mix_GetError());
                obj_this.a_tracks[track]->setState (sound_state_e::SOUND_STOPPED);
            }
        }//end else

    }//end if (obj_this.p_music_playing->isLoop ())
    else
        obj_this.a_tracks[track]->setState (sound_state_e::SOUND_STOPPED);
}//end track_finished_cb ()


/**
 * @brief Start playback of an Effect on a specific track.
 *
 * If the wanted track is occupied and:<br>
 *   - is of same importance, the previous playback will replaced by the new one.<br>
 *   - is of different importance, the non important one will be halted/dicarded.
 *
 * @param track_id - Which track_id to use for playback.<br>
 * @param ef - pointer to the Effect to play.
 *
 * @return The actual track id used to playback the Effect.
 * @retval VIRTUAL_TRACK_ID - if the track is in use and this Effect is not important.
 * @retval INVALID_TRACK_ID - if error.
 *
 * @throws std::invalid_argument - in case the provided effect is null.
 * @throws std::range_error - in case track id is outside the track range.
 * @throws std::runtime_error  - in case there is an error trying to start playback.
 */
AudioTrackID_t Audio::play (AudioTrackID_t track_id, Effect *ef)
{
    AudioTrackID_t ch_confirm = VIRTUAL_TRACK_ID;

    float distance = 0.f;
    //to centers listener coordinate on [0,0].
    glm::vec3 ref;
    //we only need a 2D reference to position the effect in the world.
    glm::vec2 norm_ref;

    _debug (TRACE, "::%s (effect in track=%u)", __FUNCTION__, track_id);

    if (!this->b_is_open)
    {
        _debug (ERROR, "Trying to use Audio, but audio device is closed!");
        return INVALID_TRACK_ID;
    }//end else if (!this->b_is_open)
    else if (!ef)
        throw std::invalid_argument (fmt::format ("{0}:{1} - Effect is null!", __LINE__, __FILE__));
    else if (track_id < 0 || track_id >= (int)this->u_track_count)
        throw std::range_error (fmt::format ("{0}:{1} - Track ID is outside the track range supported, {2} of {3}!", __LINE__, __FILE__, track_id, this->u_track_count));
    else if (!ef->u_repeat)
    {
        _debug (WARNING, "Trying to play an Effect on track %u with zero repetitions, are you sure? Nothing to play...", track_id);
        return INVALID_TRACK_ID;
    }//end else if (!ef->u_repeat)

    //decide what to do if track is already in use...
    if (this->isPlayingEffect (track_id))
    {
        if (!this->a_tracks[track_id]->b_important && ef->b_important)
        {
            ///@todo: virtualize the effect currently playing
            this->stopEffect (track_id);
        }//end if (!this->a_tracks[track_id]->b_important && ef->b_important)
        else if (this->a_tracks[track_id]->b_important && ef->b_important)
        {
            _debug (WARNING, "Track %u is busy and both effects are important, discarding new Effect.", track_id);
            return INVALID_TRACK_ID;
        }//end else if (this->a_tracks[track_id]->b_important && ef->b_important)
        else //both are not important
        {
            ///@todo: virtualize the effect that will take longer to end
            _debug (INFO, "Track %u is busy, discarding new Effect.", track_id);
            return VIRTUAL_TRACK_ID;
        }//end else
    }//end if (Mix_Playing (track_id))

    if (!ef->setState ((ef->d_fadein > 0 ? sound_state_e::SOUND_FADING_IN : sound_state_e::SOUND_PLAYING)))
        return INVALID_TRACK_ID;

    ch_confirm = Mix_FadeInChannel (track_id, ef->getSource()->getData (), 0, ef->d_fadein * 1000);
    if (ch_confirm < 0)
        throw std::runtime_error (fmt::format ("{0}:{1} - Error trying to play effect, this may or may not help:\r\n{2}", __LINE__, __FILE__, Mix_GetError()));

    this->a_tracks[ch_confirm] = ef;
    this->a_tracks[ch_confirm]->_using_track = ch_confirm;
    this->a_tracks[ch_confirm]->u_repeat_count = ef->u_repeat - 1;

    //spatial audio
    ref = ef->v3_position.get() - this->v3_microphone_position.get();
    distance = glm::length (ref);
    if (distance > this->f_min_distance)
    {
        norm_ref = glm::normalize (ref);
        Mix_SetPosition (ch_confirm, std::atan2(norm_ref.x, norm_ref.y) * 180 / 3.14,
                                     fromDistance2Volume (distance, this->f_min_distance, this->f_max_distance));
    }//end if (distance > this->f_min_distance)
    this->a_tracks[ch_confirm]->v3_position.resetFlag ();

    return ch_confirm;
}//End play ()


/**
 * @brief Start playback of a Effect on a random track.
 *
 * If there are no free channels available and:<br>
 *   - this effect is not important, it will be discarded.<br>
 *   - this effect is important, the older non important effect will be halted to make space.<br>
 *   - everybody is important, the closest to end effect will be halted and replaced by the new one.
 *
 * @param ef - pointer to the Effect to play.
 *
 * @return The actual track used to playback the Effect.
 * @retval VIRTUAL_TRACK_ID - if it was halted.
 * @retval INVALID_TRACK_ID - if error.
 */
AudioTrackID_t Audio::play (Effect *ef)
{
    int i = 0;
    AudioTrackID_t ch_confirm = VIRTUAL_TRACK_ID;

    _debug (TRACE, "::%s (effect in any track)", __FUNCTION__);

    if (!this->b_is_open)
    {
        _debug (ERROR, "Trying to use Audio, but audio device is closed!");
        return INVALID_TRACK_ID;
    }//end else if (!this->b_is_open)

    //let the Mixer find a free track.....
    ch_confirm = Mix_GroupAvailable (-1);
    if (ch_confirm >= 0)
        this->play (ch_confirm, ef);
    else if (ef->b_important)
    {
        //find a non important effect and use its place
        for (i = this->u_track_count; i--; )
        {
            if (this->a_tracks[i] && !this->a_tracks[i]->b_important)
            {
                ch_confirm = this->play (i, ef); //it will just replace the track
                break;
            }//end if (!this->a_tracks[i]->b_important)
        }//end for (i = this->u_track_count; i--; )

        if (i < 0)
            ///@todo: refuse the effect or try to find one that will end later than this one to virtualize?
            _debug (INFO, "No available track to play Effect.");
    }//end else
    else
        ///@todo: find one that will end later than this one to virtualize?
        _debug (INFO, "No available track to play Effect.");

    return ch_confirm;
}//End play ()


/**
 * @brief Pauses all the Tracks currently in playback.
 */
void Audio::pauseAllEffects ()
{
    int i = 0;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (!this->b_is_open)
    {
        _debug (WARNING, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)

    Mix_Pause (-1);
    for (i = this->u_track_count; i--; )
    {
        if (this->a_tracks[i])
            this->a_tracks[i]->setState (sound_state_e::SOUND_PAUSED);
    }//end for (i = this->u_track_count; i--; )
}//End pauseEffect (All)


/**
 * @brief Pauses the Effect playback on a specific track (if anything is playing).
 *
 * @param track - the track id to pause.
 *
 * @throws std::range_error - in case the track (0 > id > track count).
 */
void Audio::pauseEffect (AudioTrackID_t track)
{
    _debug (TRACE, "::%s (track=%u)", __FUNCTION__, track);

    if (!this->b_is_open)
    {
        _debug (WARNING, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)

    if (track < 0 || track >= (int)this->u_track_count)
        throw std::range_error (fmt::format ("{0}:{1} - Invalid track ID = {2}!", __LINE__, __FILE__, track));

    if (this->a_tracks[track] && this->a_tracks[track]->setState (sound_state_e::SOUND_PAUSED))
        Mix_Pause (track);
}//End pause (track)


/**
 * @brief Pauses the playback of a specific Effect.
 *
 * @param ef - reference to the Effect to pause.
 *
 * @throws std::range_error - in case the track info in the Effect is > track count.
 */
void Audio::pauseEffect (Effect &ef)
{
    _debug (TRACE, "::%s (Effect)", __FUNCTION__);

    if (ef._using_track < 0)
    {
        _debug (ERROR, "The effect is not under the Audio library custody.");
        return;
    }//end else if (ef._using_track == INVALID_TRACK_ID)

    this->pauseEffect (ef._using_track);
}//End pause (effect)


/**
 * @brief Resumes all the Effect Tracks currently paused.
 */
void Audio::resumeAllEffects ()
{
    int i = 0;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    Mix_Resume (-1);
    for (i = this->u_track_count; i--; )
    {
        if (this->a_tracks[i] && this->a_tracks[i]->getState() == sound_state_e::SOUND_PAUSED)
            this->a_tracks[i]->setState (sound_state_e::SOUND_PLAYING);
    }//end for (i = this->u_track_count; i--; )
}//End resumeEffect (All)


/**
 * @brief Resumes the playback on a specific track that is paused.
 *
 * @param track - the track id to resume.
 *
 * @throws std::range_error - in case the track (0 > id > track count).
 */
void Audio::resumeEffect (AudioTrackID_t track)
{
    _debug (TRACE, "::%s (track=%u)", __FUNCTION__, track);

    if (!this->b_is_open)
    {
        _debug (WARNING, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)

    if (track < 0 || track >= (int)this->u_track_count)
        throw std::range_error (fmt::format ("{0}:{1} - Invalid track ID = {2}!", __LINE__, __FILE__, track));

    if (this->a_tracks[track] && this->a_tracks[track]->setState (sound_state_e::SOUND_PLAYING))
        Mix_Resume (track);
}//end resume (track)


/**
 * @brief Resumes the playback on a specific Effect.
 *
 * @param ef - reference to the Effect to resume.
 *
 * @throws std::invalid_argument - in case the track info in the Effect is > track count.
 * @todo Try to find a "free" track to play, like virtualizing a important effect that is paused or such...
 */
void Audio::resumeEffect (Effect &ef)
{
    _debug (TRACE, "::%s (Effect)", __FUNCTION__);

    if (ef._using_track < 0)
    {
        _debug (ERROR, "The effect is not under the Audio library custody.");
        return;
    }//end else if (ef._using_track == INVALID_TRACK_ID)

    this->resumeEffect (ef._using_track);
}//End resume (Effect)


/**
 * @brief Stops the playback of all sound Effects.
 *
 * @param fade_out - time in seconds to fade out Music.
 */
void Audio::stopAllEffects (double fade_out)
{
    _debug (TRACE, "::%s (all, fade=%.4fs)", __FUNCTION__, fade_out);

    Mix_FadeOutChannel (-1, fade_out * 1000); //this should trigger the channel end callback to all playing effects...
}//End stop (all)


/**
 * @brief Stops the playback of an Effect on a specific track (it will make the track free to use).
 *
 * @param track - the track id to stop.
 * @param fade_out - time in seconds to fade out Effect (it will override the defined container fade out).
 *
 * @throws std::range_error - in case the track (0 > id > track count).
 */
void Audio::stopEffect (AudioTrackID_t track, double fade_out)
{
    _debug (TRACE, "::%s (track=%u, fade=%.4fs)", __FUNCTION__, track, fade_out);

    if (!this->b_is_open)
    {
        _debug (WARNING, "Trying to use Audio, but audio device is closed!");
        return;
    }//end else if (!this->b_is_open)
    else if (track < 0 || track >= (int)this->u_track_count)
        throw std::range_error (fmt::format ("{0}:{1} - Invalid track ID = {2}!", __LINE__, __FILE__, track));

    if (this->a_tracks[track] && this->a_tracks[track]->setState (sound_state_e::SOUND_STOPPED))
        Mix_FadeOutChannel (track, fade_out * 1000); //this should trigger the channel end callback...
}//End stop (track)


/**
 * @brief Stop the playback of a specific Effect.
 *
 * @param ef - reference to the Effect to stop.
 * @param fade_out - time in seconds to fade out Effect (it will override the defined container fade out).
 *
 * @throws std::range_error - in case the track info in the Effect is > track count.
 */
void Audio::stopEffect (Effect &ef, double fade_out)
{
    _debug (TRACE, "::%s (Effect, fade=%.4fs)", __FUNCTION__, fade_out);

    if (ef._using_track < 0)
    {
        _debug (ERROR, "The effect is not under the Audio library custody.");
        return;
    }//end else if (ef._using_track == INVALID_TRACK_ID)

    this->stopEffect (ef._using_track, fade_out);
}//End stop (Effect)

